package cn.ijero.game2048

import com.soywiz.klock.seconds
import com.soywiz.korev.Key
import com.soywiz.korge.Korge
import com.soywiz.korge.animate.Animator
import com.soywiz.korge.animate.animateSequence
import com.soywiz.korge.html.Html
import com.soywiz.korge.input.SwipeDirection
import com.soywiz.korge.input.keys
import com.soywiz.korge.input.onClick
import com.soywiz.korge.input.onSwipe
import com.soywiz.korge.service.storage.storage
import com.soywiz.korge.tween.get
import com.soywiz.korge.ui.*
import com.soywiz.korge.view.*
import com.soywiz.korim.color.Colors
import com.soywiz.korim.color.RGBA
import com.soywiz.korim.font.BitmapFont
import com.soywiz.korim.font.readBitmapFont
import com.soywiz.korim.format.readBitmap
import com.soywiz.korim.text.TextAlignment
import com.soywiz.korio.async.ObservableProperty
import com.soywiz.korio.async.launchImmediately
import com.soywiz.korio.file.std.resourcesVfs
import com.soywiz.korma.geom.Rectangle
import com.soywiz.korma.geom.vector.roundRect
import com.soywiz.korma.interpolation.Easing
import kotlin.properties.Delegates
import kotlin.random.Random

var font: BitmapFont by Delegates.notNull()
var cellSize = 0.0
var fieldSize = 0.0
var leftIndent = 0.0
var topIndent = 0.0
var map = PositionMap()
val blocks = mutableMapOf<Int, Block>()
var freeId = 0
var isAnimationRunning = false
var isGameOver = false
val rx = 5.0
val score = ObservableProperty(0)
val best = ObservableProperty(0)

suspend fun main() = Korge(width = 480, height = 640, bgcolor = RGBA(253, 247, 240), title = "2048") {
    font = resourcesVfs["fonts/clear_sans.fnt"].readBitmapFont()
    cellSize = views.virtualWidth / 5.0
    fieldSize = cellSize * 4 + 50
    leftIndent = (views.virtualWidth - fieldSize) * 0.5
    topIndent = 150.0

    val storage = views.storage
    best.update(storage.getOrNull("best")?.toInt() ?: 0)

    score.observe {
        if (it > best.value) best.update(it)
    }

    best.observe {
        storage["best"] = it.toString()
    }

    // bg
    val bgField = roundRect(fieldSize, fieldSize, rx, fill = Colors["#b9aea0"]) {
        position(leftIndent, topIndent)
    }

    // cells
    graphics {
        position(leftIndent, topIndent)
        fill(Colors["#cec0b2"]) {
            for (i in 0..3) {
                for (j in 0..3) {
                    roundRect(10.0 + (10.0 + cellSize) * i, 10.0 + (10.0 + cellSize) * j, cellSize, cellSize, rx)
                }
            }
        }
    }

    // logo bg
    val bgLogo = roundRect(cellSize, cellSize, rx, fill = Colors["#edc403"]) {
        position(leftIndent, 30.0)
    }

    // logo text
    textOld("2048", cellSize * 0.5, Colors.WHITE, font).centerOn(bgLogo)

    // best bg
    val bgBest = roundRect(cellSize * 1.5, cellSize * 0.8, rx, fill = Colors["#bbae9e"]) {
        alignRightToRightOf(bgField)
        alignTopToTopOf(bgLogo)
    }

    // best title
    text("BEST", cellSize * 0.25, RGBA(239, 226, 210), font) {
        centerXOn(bgBest)
        alignTopToTopOf(bgBest, 5.0)
    }

    // best score
    text(best.value.toString(), cellSize * 0.5, Colors.WHITE, font) {
        setTextBounds(Rectangle(0.0, 0.0, bgBest.width, cellSize - 24.0))
        alignment = TextAlignment.MIDDLE_CENTER
        alignTopToTopOf(bgBest, 12.0)
        centerXOn(bgBest)

        best.observe {
            text = it.toString()
        }
    }

    // score bg
    val bgScore = roundRect(cellSize * 1.5, cellSize * 0.8, rx, fill = Colors["#bbae9e"]) {
        alignRightToLeftOf(bgBest, 24.0)
        alignTopToTopOf(bgLogo)
    }

    // score title
    text("SCORE", cellSize * 0.25, RGBA(239, 226, 210), font) {
        centerXOn(bgScore)
        alignTopToTopOf(bgScore, 5.0)
    }

    // score
    text(score.value.toString(), cellSize * 0.5, Colors.WHITE, font) {
        setTextBounds(Rectangle(0.0, 0.0, bgScore.width, cellSize - 24.0))
        alignment = TextAlignment.MIDDLE_CENTER
        alignTopToTopOf(bgScore, 12.0)
        centerXOn(bgScore)

        score.observe {
            text = it.toString()
        }
    }

    val restartImg = resourcesVfs["image/restart.png"].readBitmap()
    val undoImg = resourcesVfs["image/undo.png"].readBitmap()

    val btnSize = cellSize * 0.3

    // restart
    val restartBlock = container {
        val bg = roundRect(btnSize, btnSize, rx, fill = RGBA(185, 174, 160))
        image(restartImg) {
            size(btnSize * 0.8, btnSize * 0.8)
            centerOn(bg)
        }
        alignTopToBottomOf(bgBest, 5.0)
        alignRightToRightOf(bgField)
        onClick {
            this@Korge.restart()
        }
    }

    val undoBlock = container {
        val bg = roundRect(btnSize, btnSize, rx, fill = RGBA(185, 174, 160))
        image(undoImg) {
            size(btnSize * 0.6, btnSize * 0.6)
            centerOn(bg)
        }
        alignTopToBottomOf(bgBest, 5.0)
        alignRightToLeftOf(restartBlock, 5.0)
    }

    generateBlock()

    keys.down {
        when (it.key) {
            Key.LEFT -> {
                moveBlocksTo(Direction.LEFT)
            }
            Key.UP -> {
                moveBlocksTo(Direction.TOP)
            }
            Key.RIGHT -> {
                moveBlocksTo(Direction.RIGHT)
            }
            Key.DOWN -> {
                moveBlocksTo(Direction.BOTTOM)
            }
            else -> {
                // println(it.key.name)
            }
        }
    }

    onSwipe(-30.0) {
        when (it.direction) {
            SwipeDirection.LEFT -> {
                moveBlocksTo(Direction.LEFT)
            }
            SwipeDirection.TOP -> {
                moveBlocksTo(Direction.TOP)
            }
            SwipeDirection.RIGHT -> {
                moveBlocksTo(Direction.RIGHT)
            }
            SwipeDirection.BOTTOM -> {
                moveBlocksTo(Direction.BOTTOM)
            }
        }
    }

}

fun Stage.moveBlocksTo(direction: Direction) {
    if (isAnimationRunning) return
    if (!map.hasAvailableMoves()) {
        // game over
        if (!isGameOver) {
            isGameOver = true
            showGameOver {
                isGameOver = false
                restart()
            }
        }
        return
    }

    val moves = mutableListOf<Pair<Int, Position>>()
    val merges = mutableListOf<Triple<Int, Int, Position>>()

    val newMap = calculateNewMap(map.copy(), direction, moves, merges)

    if (map != newMap) {
        isAnimationRunning = true
        showAnimation(moves, merges) {
            map = newMap
            generateBlock()
            isAnimationRunning = false

            // update score
            val points = merges.sumBy { numberFor(it.first).value }
            score.update(score.value + points)
        }
    }
}

fun calculateNewMap(
    map: PositionMap,
    direction: Direction,
    moves: MutableList<Pair<Int, Position>>,
    merges: MutableList<Triple<Int, Int, Position>>
): PositionMap {
    val newMap = PositionMap()
    val startIndex = when (direction) {
        Direction.LEFT, Direction.TOP -> 0
        Direction.RIGHT, Direction.BOTTOM -> 3
    }

    var columnRow = startIndex

    fun newPosition(line: Int) = when (direction) {
        Direction.LEFT -> {
            Position(columnRow++, line)
        }
        Direction.RIGHT -> {
            Position(columnRow--, line)
        }
        Direction.TOP -> {
            Position(line, columnRow++)
        }
        Direction.BOTTOM -> {
            Position(line, columnRow--)
        }
    }

    for (line in 0..3) {
        var curPos = map.getNotEmptyPositionFrom(direction, line)
        columnRow = startIndex
        while (curPos != null) {
            val newPos = newPosition(line)
            val curId = map[curPos.x, curPos.y]
            map[curPos.x, curPos.y] = -1

            val nextPos = map.getNotEmptyPositionFrom(direction, line)
            val nextId = nextPos?.let {
                map[it.x, it.y]
            }

            // 如果两个块相同
            if (nextId != null && numberFor(curId) == numberFor(nextId)) {
                // 合并
                map[nextPos.x, nextPos.y] = -1
                newMap[newPos.x, newPos.y] = curId
                merges += Triple(curId, nextId, newPos)
            } else {
                // 添加旧的块
                newMap[newPos.x, newPos.y] = curId
                moves += Pair(curId, newPos)
            }
            curPos = map.getNotEmptyPositionFrom(direction, line)
        }
    }

    return newMap
}

fun numberFor(blockId: Int) = blocks[blockId]!!.number

fun Stage.showAnimation(
    moves: MutableList<Pair<Int, Position>>,
    merges: MutableList<Triple<Int, Int, Position>>,
    onEnd: () -> Unit
) = launchImmediately {
    animateSequence {
        parallel {
            moves.forEach { (id, pos) -> blocks[id]!!.moveTo(columnX(pos.x), rowY(pos.y), 0.15.seconds, Easing.LINEAR) }
            merges.forEach { (id1, id2, pos) ->
                sequence {
                    parallel {
                        blocks[id1]!!.moveTo(columnX(pos.x), rowY(pos.y), 0.15.seconds, Easing.LINEAR)
                        blocks[id2]!!.moveTo(columnX(pos.x), rowY(pos.y), 0.15.seconds, Easing.LINEAR)
                    }
                    block {
                        val nextNumber = numberFor(id1).next()
                        deleteBlock(id1)
                        deleteBlock(id2)
                        createNewBlockWithId(id1, nextNumber, pos)
                    }
                    sequenceLazy {
                        animateScale(blocks[id1]!!)
                    }
                }
            }
        }
        block {
            onEnd()
        }
    }
}

fun Animator.animateScale(block: Block) {
    val x = block.x
    val y = block.y
    val scale = block.scale
    tween(block::x[x - 4], block::y[y - 4], block::scale[scale + 0.1], time = 0.1.seconds, easing = Easing.LINEAR)
    tween(block::x[x], block::y[y], block::scale[scale], time = 0.1.seconds, easing = Easing.LINEAR)
}

fun deleteBlock(id: Int) {
    blocks.remove(id)!!.removeFromParent()
}

fun Container.restart() {
    map = PositionMap()
    blocks.values.forEach { it.removeFromParent() }
    blocks.clear()
    score.update(0)
    generateBlock()
}

fun Container.showGameOver(onRestart: () -> Unit) = container {
    val textFormat = TextFormat(
        color = RGBA(0, 0, 0),
        size = 40,
        font = font
    )
    val textSkin = TextSkin(
        normal = textFormat,
        over = textFormat.copy(color = RGBA(90, 90, 90)),
        down = textFormat.copy(RGBA(120, 120, 120))
    )

    fun restart() {
        this@container.removeFromParent()
        onRestart()
    }

    position(leftIndent, topIndent)

    roundRect(fieldSize, fieldSize, rx, fill = Colors["#FFFFFF33"])
    text("Game Over", 60.0, Colors.BLACK, font) {
        centerBetween(0.0, 0.0, fieldSize, fieldSize)
        y -= 60
    }

    uiText("Try again", 120.0, 35.0, textSkin) {
        centerBetween(0.0, 0.0, fieldSize, fieldSize)
        y += 20
        onClick {
            restart()
        }
    }

    keys.down {
        when (it.key) {
            Key.SPACE, Key.ENTER -> {
                restart()
            }
            else -> {
                // nothing...
            }
        }
    }

}

/**
 * 通过id创建一个新的块，并添加到map中
 *
 **/
fun Container.createNewBlockWithId(id: Int, number: Number, position: Position) {
    blocks[id] = block(number).position(columnX(position.x), rowY(position.y))
}

/**
 * 创建一个新块，并返回id
 *
 **/
fun Container.createNewBlock(number: Number, position: Position): Int {
    val id = freeId++
    createNewBlockWithId(id, number, position)
    return id
}

/**
 * 随机生成一个新的块
 *
 **/
fun Container.generateBlock() {
    val position = map.getRandomFreePosition() ?: return
    val number = if (Random.nextDouble() < 0.9) Number.ZERO else Number.ONE
    val newId = createNewBlock(number, position)
    map[position.x, position.y] = newId
}

/**
 * 通过块索引计算在行内的精确位置（第几列）
 *
 **/
fun columnX(number: Int) = leftIndent + 10 + (cellSize + 10) * number

/**
 * 通过块索引计算在列内的精确位置（第几行）
 *
 **/
fun rowY(number: Int) = topIndent + 10 + (cellSize + 10) * number